fix(driver-sql): 空 $and/$or/$not 按布尔单位元编译,$or: [] 不再返回全表 (#5134) - #5243
Merged
Conversation
applyFilterCondition 把每个组合子都编译成一个 knex 分组回调,而 knex 对
「一个子句都没加进去的分组」不产出 SQL。于是「这个组是空的」和「这个组
已被满足」编译成了同一条查询。丢弃子句不等于套用单位元,而两个单位元的
方向相反:空 $and 是 TRUE、空 $or 是 FALSE、$not 空子分组是 FALSE。旧代码
对三者一律给全表;$and 恰好正确只是因为「丢掉」在 AND 侧碰巧等价于 TRUE。
放松的两格是安全相关的:$or: [] 最常见的来源正是「本该有条件、但循环一个
析取项都没填进去」的 RLS read scope,当成全表意味着本该看不到任何行的人
拿到了整表。formula 的 matchesFilterCondition 与 driver-memory 三条本来
就都对,driver-sql 是唯一的例外。
配套的形状拒收是同一处修复不可分的一半:套用单位元的前提是「编译成空」
只剩一个成因。$or: [null] / ['x'] / [[…]] / [new Date()] 以前同样无痕消失,
不先拦掉就上单位元会把它们从「被静默忽略」升级成「匹配所有行」,比原 bug
更坏。$and/$or 的元素与 $not 的操作数现在必须是 plain object 的 filter
节点,否则按 ADR-0112 响亮拒收(INVALID_FILTER / 400,报错指明位置)。
原型判定是关键的一半:Date/RegExp/class 实例都满足 typeof === 'object'
却枚举为空,被接受就会被读成 TRUE。
判定是结构性的(编译前先把整棵树归约成 true/false/clause 三值),而不是
「编译完再问 knex 有没有产出」—— 原缺陷本身就是后者那种观察,而观察分不清
「因为本来就是空」和「因为有东西没编译出来」。结构判定没有这个盲区,并且
保证编译器打开的每个分组都至少收到一条子句。
非空的 $and/$or/$not 编译方式完全未变。{ field: {} } 刻意不裁决(归约把带
字段键的节点一律判为 clause),该分叉另记 #5240;一致性表扩条另记 #5239。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…-driver-boolean-identity
os-zhuang
marked this pull request as ready for review
August 4, 2026 12:10
os-zhuang
enabled auto-merge
August 4, 2026 12:10
This was referenced Aug 4, 2026
Closed
read-scope-sql 的
$not 有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且 { $not: {} } 编译成空 → RLS 整表放行
#5297
Closed
Open
Merged
Closed
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5134
含 07:29Z 注记并入的 #5146 分叉二(
$not空子分组 → 方言 FALSE)。#5146 剩余的 NULL 语义分叉未触碰,非空$not的编译方式逐字节未变。问题
applyFilterCondition把每个组合子都编译成一个 knex 分组回调,而 knex 对「一个子句都没加进去的分组」不产出任何 SQL。于是**「这个组是空的」和「这个组已被满足」编译成了同一条查询**。丢弃子句不等于套用单位元,而两个单位元的方向是相反的:{ $and: [] }{ $or: [] }{ $or: [{a}, {}] }{}是 TRUE 析取项 → 全部行(a = ?){ $not: {} }NOT TRUE ≡ FALSE→ 零行$and: []恰好正确的理由不是代码理解了单位元,而是「丢掉」在 AND 侧碰巧等价于 TRUE —— 同一段代码在 OR 与 NOT 侧就必然错。放松的那两格是安全相关的:
$or: []最常见的来源正是「本该有条件、但循环一个析取项都没填进去」的 RLS read scope,把它当成全表意味着本该看不到任何行的人拿到了整表。同仓另外两个后端(
formula的matchesFilterCondition、driver-memory)三条本来就都对,driver-sql是唯一的例外。修法
1. 结构性三值归约,而不是观察式判空
新增
reduceFilterNode/reduceFilterKey,在产出任何 SQL 之前把整棵 filter 树归约成'true'/'false'/'clause'。这一点是刻意的:原缺陷本身就是一次「编译完再看分组里有没有东西」的观察,而观察分不清「因为本来就是空」和「因为有东西没编译出来」。结构判定没有这个盲区。副产品:编译器打开的每一个分组都保证至少收到一条子句 —— TRUE 组合子整个跳过,
$and里的 TRUE 成员与$or里的 FALSE 成员按各自单位元丢弃,归约为 FALSE 的节点根本走不到发射循环。knex 再没有机会静默丢弃一个组。FALSE 常量用
1 = 0:read-scope-sql.ts对空$in用的就是它,knex 自己渲染空whereIn也是它,三个方言通吃,无 binding,且查询仍是普通 SELECT(LIMIT/ORDER BY/聚合都照常)。2. 形状拒收 —— 与单位元不可分的一半
套用单位元的前提是「编译成空」只剩一个成因。在此之前
$or: [null]、$or: ['x']、$or: [[…]]、$or: [new Date()]同样会无痕消失;不先拦掉它们就上单位元,会把它们从「被静默忽略」升级成「匹配所有行」,比原 bug 更坏(cloud#1073 踩过的那一脚)。现在
$and/$or的元素与$not的操作数必须是 plain object 的 filter 节点,否则按 ADR-0112 响亮拒收 ——INVALID_FILTER/ 400、消息指明出错位置(filter.$or[1])、不带[sql-driver]前缀,与本 driver 既有的unsupportedFilterError同一个信封。$and: 'x'这类非数组操作数也不再被当成一个名为$and的字段列。原型判定是关键的一半:
Date/RegExp/ class 实例都满足typeof x === 'object' && !Array.isArray(x)却枚举为空 —— 若被接受,归约会把它们读成 TRUE 并交出整张表。测试里有一条 class 实例专门钉这个。刻意不做的事
{ field: {} }(零个操作符的字段约束)不裁决。 归约把任何带字段键的节点一律判为'clause'(而非'true'),所以该形状编译结果与修前逐字节相同。它在同仓有三个答案(driver-sql 组合子内 TRUE / driver-sql 顶层抛INVALID_FILTER/ formula 与 driver-memory FALSE),定调需维护者拍板 —— 已另立{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240,代码里留了注释指向它。packages/spec/**零改动。FILTER_LOGIC_CASES扩条归 spec 车道,已另立 [spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239,并在单里写明 driver-mongodb 必须同 PR 落地单位元归约(它把空数组原样透传,MongoDB 直接报错 —— 第三种行为),否则表一加就把 mongodb 的 cell 打红。formula/driver-memory/driver-mongodb。验证
修前(新 pin 文件跑在未改动的
main上)——bug 逐条复现:整个 pin 文件修前
22 failed | 12 passed (34);其中{ $or: [null] }修前 resolve 出全部 3 行(「垃圾静默匹配所有行」的实证)。修后:
SqliteWasmDriver extends SqlDriver,继承同一个方法,一并跑过:(含它自己的
sqlite-wasm-filter-logic-conformance.test.ts。)sql-driver-or-filter.test.ts(#3774 的一致性 sweep)与sql-driver-filter-no-silent-drop.test.ts(#3948 的响亮拒收)全绿 —— 本次改动与它们钉住的行为不冲突。用户可见性
changeset 已加(
patch),并在正文里明确写出这是查询行为变更:{ $or: [] }从返回整张表变成返回零行,依赖旧行为的代码依赖的是一个 filter 旁路,RLS 影响单列说明。🤖 Generated with Claude Code
https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Generated by Claude Code